/*
 * #%L
 * java model
 * %%
 * Copyright (C) 2009 - 2011 prefabware.com Stefan Isele
 * %%
 * All Rights Reserved.
 * This program is dual-licensed under the GNU General Public license
 * or a commercial license.
 * You may select to use one or the other of these licenses.
 * To obtain a commercial license please contact Stefan Isele at www.stefan-isele.de
 * Any modifications to this file must keep this entire header intact.
 * Redistribution, except as permitted by the selected license, is prohibited.
 * #L%
 */
package com.prefabware.jmodel.typefactory;

import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import com.prefabware.commons.CollectionUtil;
import com.prefabware.commons.Condition;
import com.prefabware.commons.JavaUtil;
import com.prefabware.commons.Message;
import com.prefabware.commons.QualifiedName;
import com.prefabware.commons.collection.Filter;
import com.prefabware.commons.reflection.ClassReflector;
import com.prefabware.jmodel.JClass;
import com.prefabware.jmodel.JEnumeration;
import com.prefabware.jmodel.JInterface;
import com.prefabware.jmodel.JModifiers;
import com.prefabware.jmodel.JParameterizedType;
import com.prefabware.jmodel.JThrowable;
import com.prefabware.jmodel.JType;
import com.prefabware.jmodel.JTypeDeclaration;
import com.prefabware.jmodel.JVisibility;
import com.prefabware.jmodel.annotation.JAnnotationClass;

/**
 * to create instances of JTYpe and its superclasses.Delegates all requests to
 * its {@link TypeFactoryDelegee}
 * 
 * @author sisele_job
 */
public class TypeFactory {
	private final static Logger LOGGER = Logger.getLogger(TypeFactory.class
			.getName());


	protected static JClass createFinalClass(Class<?> clazz) {
		return new JClass(QualifiedName.create(clazz),
				new JTypeDeclaration(QualifiedName.create(clazz), JModifiers.FINAL,
						JVisibility.PUBLIC, null));
	}
	protected static JInterface createInterface(Class<?> clazz) {
		return new JInterface(QualifiedName.create(clazz),
				new JTypeDeclaration(QualifiedName.create(clazz), JModifiers.FINAL,
						JVisibility.PUBLIC, null));
	}
	
	/**
	 * JClass for {@link Void} useless !
	 */
	@Deprecated
	public static JClass Void_ = createFinalClass(Void.class);
	public static JInterface Collection = createInterface(java.util.Collection.class);
	public static JInterface List = createInterface(java.util.List.class);
	public static JInterface Set = createInterface(java.util.Set.class);
	public static JInterface Map = createInterface(java.util.Map.class);

	public static JClass byte_ = createPrimitive(byte.class);
	public static JClass short_ = createPrimitive(short.class);
	public static JClass int_ = createPrimitive(int.class);
	public static JClass long_ = createPrimitive(long.class);
	public static JClass char_ = createPrimitive(char.class);
	public static JClass float_ = createPrimitive(float.class);
	public static JClass double_ = createPrimitive(double.class);
	public static JClass boolean_ = createPrimitive(boolean.class);
	public static JClass Object_ = new JClass(QualifiedName.create(Object.class),
			new JTypeDeclaration(QualifiedName.create(Object.class), JModifiers.NONE,
					JVisibility.PUBLIC, null));;
	/**
	 * JClass for {@link void}
	 */
	public static JClass void_ = createPrimitive(void.class);

	private static JClass createPrimitive(Class<?> clazz) {
		return new JClass(QualifiedName.create(clazz), new JTypeDeclaration(
				QualifiedName.create(clazz), JModifiers.FINAL, JVisibility.PUBLIC,
				null));
	}

	protected static class CacheKeyClass {
		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((clazz == null) ? 0 : clazz.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			CacheKeyClass other = (CacheKeyClass) obj;
			if (clazz == null) {
				if (other.clazz != null)
					return false;
			} else if (!clazz.equals(other.clazz))
				return false;
			return true;
		}

		Class<?> clazz;
		private QualifiedName qualifiedName;

		protected CacheKeyClass(Class<?> clazz) {
			super();
			this.clazz = clazz;
		}

		public QualifiedName getQualifiedName() {
			if (qualifiedName == null) {
				qualifiedName = new QualifiedName(JavaUtil.getName(clazz));
			}
			return qualifiedName;
		}

		@Override
		public String toString() {
			return "CacheKeyClass [clazz=" + clazz + "]";
		}

	}

	private static class CacheKeyQn {
		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result
					+ ((qualifiedName == null) ? 0 : qualifiedName.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			CacheKeyQn other = (CacheKeyQn) obj;
			if (qualifiedName == null) {
				if (other.qualifiedName != null)
					return false;
			} else if (!qualifiedName.equals(other.qualifiedName))
				return false;
			return true;
		}

		QualifiedName qualifiedName;

		private CacheKeyQn(QualifiedName qualifiedName) {
			super();
			this.qualifiedName = qualifiedName;
		}
	}

	private List<TypeFactoryDelegee> delegees;
	private Map<CacheKeyClass, JType> typeCache;
	private Map<CacheKeyQn, JType> typeCacheByName;

	/**
	 * creates a new Factory delegating to the given {@link TypeFactoryDelegee}s
	 * allways adds {@link DefaultTypeFactoryDelegee} as last factory
	 * 
	 * @param delegees
	 */
	public TypeFactory(TypeFactoryDelegee... delegees) {
		super();
		this.delegees = new ArrayList<TypeFactoryDelegee>();
		for (TypeFactoryDelegee delegee : delegees) {
			this.addDelegee(delegee);
		}

		// now add the default delegees
		this.addDelegee(new JClassTypeFactoryDelegee());
		this.addDelegee(new JInterfaceTypeFactoryDelegee());
		this.addDelegee(new JEnumerationTypeFactoryDelegee());
		this.addDelegee(new JAnnotationClassTypeFactoryDelegee(this));
		this.addDelegee(new JThrowableTypeFactoryDelegee());
		this.typeCache = new HashMap<CacheKeyClass, JType>();
		this.typeCacheByName = new HashMap<CacheKeyQn, JType>();

		// put the static accessible JTypes into the cache
		cache(new CacheKeyClass(Void.class), Void_);
		cache(new CacheKeyClass(java.util.Collection.class), Collection);
		cache(new CacheKeyClass(java.util.List.class), List);
		cache(new CacheKeyClass(java.util.Set.class), Set);
		cache(new CacheKeyClass(java.util.Map.class), Map);
		cache(new CacheKeyClass(byte.class), byte_);
		cache(new CacheKeyClass(short.class), short_);
		cache(new CacheKeyClass(int.class), int_);
		cache(new CacheKeyClass(long.class), long_);
		cache(new CacheKeyClass(char.class), char_);
		cache(new CacheKeyClass(float.class), float_);
		cache(new CacheKeyClass(double.class), double_);
		cache(new CacheKeyClass(boolean.class), boolean_);
		cache(new CacheKeyClass(void.class), void_);
		cache(new CacheKeyClass(Object.class), Object_);
	}

	private void addDelegee(TypeFactoryDelegee delegee) {
		this.delegees.add(delegee);
		delegee.initialize(this);
	}

	protected void cache(CacheKeyClass key, JType jType) {
		typeCache.put(key, jType);
		cache(new CacheKeyQn(key.getQualifiedName()), jType);
	}

	private void cache(CacheKeyQn key, JType jType) {
		typeCacheByName.put(key, jType);
	}

	/**
	 * @param type
	 * @return the first typeFactory that accepts the given type
	 */
	private TypeFactoryDelegee findDelegee(final Class<? extends JType> type,
			final QualifiedName qualifiedName) {
		Filter<? super TypeFactoryDelegee> acceptTypeFilter = new Filter<TypeFactoryDelegee>() {

			@Override
			public boolean includes(TypeFactoryDelegee candidate) {
				return candidate.acceptType(type, qualifiedName);
			}
		};
		return CollectionUtil.first(this.delegees, acceptTypeFilter);
	}

	/**
	 * gets or creates an instance of JClass or JInterface with the given name
	 * 
	 * @param <TYPE>
	 * @param typeClass
	 *            the type to return JClass.class or JInterface.class
	 * @param qualifiedName
	 *            the qualified name of the required JType
	 * @param componentType if the JType to create is an array, this is the element type of the array
	 * @see Class#getComponentType()
	 * can be null
	 * @param modifiers
	 *            
	 * @param visibility
	 *           
	 * @param typeArguments
	 *            the type parameters if the required JType extends a generic superclass,
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public <TYPE extends JType> TYPE getJType(Class<TYPE> typeClass,
			QualifiedName qualifiedName, JType componentType,
			JModifiers modifiers, JVisibility visibility,
			JType... typeArguments) {
		CacheKeyQn key = new CacheKeyQn(qualifiedName);
		if (typeCacheByName.containsKey(key)) {
			return (TYPE) typeCacheByName.get(key);
		}
		TypeFactoryDelegee delegee = findDelegee(typeClass, qualifiedName);
		JTypeDeclaration declaration = new JTypeDeclaration(
				qualifiedName, modifiers, visibility,
				componentType);
		declaration.setTypeArguments(Arrays.asList(typeArguments));
		// the maven compiler wants the following cast, eclipse does not need it
		// !
		TYPE newJType = (TYPE) delegee.newJType(qualifiedName, declaration,
				typeArguments);
		cache(key, newJType);
		return newJType;
	}

	/**
	 * gets or creates a JType for the given class
	 * 
	 * @param forClass
	 *            the Java class a JType must be returned for
	 * @param actualTypeArgumentsi
	 *            , the actual type arguments, if generic
	 * @return a JAnnotationClass if forClass is an an annotation a JInterface
	 *         if forClass is an interface a JClass in all other cases
	 * 
	 */
	@SuppressWarnings("unchecked")
	public <T extends JType> T getJType(Class<?> forClass) {
		return (T) getJType(null, forClass);
	}

	/**
	 * to calculate the Class to use for the given Type
	 * @param type
	 * @return the Class for the given type
	 */
	protected JParameterizedType calcClass(Type type) {
		JParameterizedType t1 = null;
		if (type instanceof Class) {
			Class<?>clazz=(Class<?>) type;
			JType jType = getJType(clazz);
			t1 = new JParameterizedType(jType);
		} else if (type instanceof GenericArrayType) {
			GenericArrayType gt = (GenericArrayType) type;
			t1 = calcClass(gt.getGenericComponentType());
		} else if (type instanceof ParameterizedType) {
			ParameterizedType pt = (ParameterizedType) type;
			//the rawtype is allways a class !
			//TODO isnt that useless, to scan all actualTypeArgs and
			//finally just use the RawType ?
			List<JType> typeArgs = new ArrayList<JType>();
			for (Type actualType : pt.getActualTypeArguments()) {
				if (actualType != type) {
					typeArgs.add(calcClass(actualType).getRawType());
				} else {
					//happend with java.net.InetAddress.Cache.Type
					typeArgs.add(getJType(Object.class));
				}
			}
			t1 = new JParameterizedType(getJType((Class<?>) pt.getRawType()),
					typeArgs.toArray(new JType[0]));
		} else if (type instanceof TypeVariable) {
			t1 = new JParameterizedType(getJType(Object.class));
		} else if (type instanceof WildcardType) {
			t1 = new JParameterizedType(getJType(Object.class));
		}
		return t1;
	}

	/**
	 * gets or creates a JType for the given class use this method to get e.g. a
	 * Bean for a class in all other cases use
	 * {@link #getJType(Class, JType...)}
	 * 
	 * @param jtypeClass
	 *            the JType to return. If another JType was allready cached for
	 *            forClass, an Exception will be thrown.
	 * @param forClass
	 *            the Java class a JType must be returned for
	 * @param actualTypeArgumentsi
	 *            , the actual type arguments, if generic
	 * @return
	 */
	/**
	 * @param <T>
	 * @param jtypeClass
	 * @param forClass
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public <T extends JType> T getJType(Class<? extends JType> jtypeClass,
			Class<?> forClass) {
		Condition.notNull(forClass, "..class");
		// consider cache
		CacheKeyClass cacheKey = new CacheKeyClass(forClass);
		if (typeCache.containsKey(cacheKey)) {
			LOGGER.fine("found in cache( " + cacheKey);
			JType cachedType = typeCache.get(cacheKey);
			// compare the cached JType with the requested one, only if the
			// caller specified it
			Condition
					.isTrue(jtypeClass == null
							|| jtypeClass.isAssignableFrom(cachedType
									.getClass()),
							Message.format(
									"requesting a wrong JType for %s.\n Now requesting %s, but %s was allready cached for it",
									forClass, jtypeClass, cachedType));
			return (T) cachedType;
		}
		if (jtypeClass == null) {
			// determine the jtype to create, if the caller did not specify it
			jtypeClass = determineJTypeClass(forClass);
			
		}
		LOGGER.fine("getJType( " + jtypeClass.getSimpleName() + " forClass "
				+ forClass.getName());

		QualifiedName qn = cacheKey.getQualifiedName();
		TypeFactoryDelegee delegee = findDelegee(jtypeClass, qn);
		JTypeClassVisitor visitor = delegee.getVisitor();
		ClassReflector reflector = new ClassReflector(forClass);
		//will cache the new JType as soon as possible
		//to avoid loops
		reflector.visitClass(visitor);
		T jType = (T) visitor.getJType();
		// cache again, paranoiac
		cache(cacheKey, jType);

		// use the typeFactories visitor to visit the Class and create
		// fields,methods, constructors
		// through reflection
		reflector.visitFields(visitor);
		reflector.visitMethods(visitor);
		reflector.visitConstructors(visitor);

		return jType;
	}

	public Class<? extends JType> determineJTypeClass(Class<?> forClass) {
		Class<? extends JType> jtypeClass;
		if (forClass.isAnnotation()) {
			jtypeClass = JAnnotationClass.class;
		} else if (forClass.isInterface()) {
			jtypeClass = JInterface.class;
		} else if (forClass.isEnum()) {
			jtypeClass = JEnumeration.class;
		} else if (Throwable.class.isAssignableFrom(forClass)) {
			jtypeClass = JThrowable.class;
		} else {
			jtypeClass = JClass.class;
		}
		return jtypeClass;
	}

}
